/* tolua: event functions
** Support code for Lua bindings.
** Written by Waldemar Celes
** TeCGraf/PUC-Rio
** Apr 2003
** $Id: $
*/

/* This code is free software; you can redistribute it and/or modify it.
** The software provided hereunder is on an "as is" basis, and
** the author has no obligation to provide maintenance, support, updates,
** enhancements, or modifications.
*/

#include <stdio.h>

#include "tolua++.h"

/* Store at ubox
    * It stores, creating the corresponding table if needed,
    * the pair key/value in the corresponding ubox table
*/
static void storeatubox( lua_State* L, int lo ) {
#ifdef LUA_VERSION_NUM
#if LUA_VERSION_NUM > 501
    lua_getuservalue( L, lo );
#else
    lua_getfenv( L, lo );
#endif

    if ( lua_rawequal( L, -1, TOLUA_NOPEER ) ) {
        lua_pop( L, 1 );
        lua_newtable( L );
        lua_pushvalue( L, -1 );
#if LUA_VERSION_NUM > 501
        lua_setuservalue( L, lo );  /* stack: k,v,table  */
#else
        lua_setfenv( L, lo ); /* stack: k,v,table  */
#endif
    };

    lua_insert( L, -3 );

    lua_settable( L,
                  -3 ); /* on lua 5.1, we trade the "tolua_peers" lookup for a settable call */

    lua_pop( L, 1 );

#else
    /* stack: key value (to be stored) */
    lua_pushstring( L, "tolua_peers" );

    lua_rawget( L, LUA_REGISTRYINDEX );     /* stack: k v ubox */

    lua_pushvalue( L, lo );

    lua_rawget( L, -2 );                    /* stack: k v ubox ubox[u] */

    if ( !lua_istable( L, -1 ) ) {
        lua_pop( L, 1 );                       /* stack: k v ubox */
        lua_newtable( L );                     /* stack: k v ubox table */
        lua_pushvalue( L, 1 );
        lua_pushvalue( L, -2 );                /* stack: k v ubox table u table */
        lua_rawset( L, -4 );                   /* stack: k v ubox ubox[u]=table */
    }

    lua_insert( L, -4 );                    /* put table before k */
    lua_pop( L, 1 );                        /* pop ubox */
    lua_rawset( L, -3 );                    /* store at table */
    lua_pop( L, 1 );                        /* pop ubox[u] */
#endif
}

/* Module index function
*/
static int module_index_event( lua_State* L ) {
    lua_pushstring( L, ".get" );
    lua_rawget( L, -3 );

    if ( lua_istable( L, -1 ) ) {
        lua_pushvalue( L, 2 ); /* key */
        lua_rawget( L, -2 );

        if ( lua_iscfunction( L, -1 ) ) {
            lua_call( L, 0, 1 );
            return 1;
        }
        else if ( lua_istable( L, -1 ) ) {
            return 1;
        }
    }

    /* call old index meta event */
    if ( lua_getmetatable( L, 1 ) ) {
        lua_pushstring( L, "__index" );
        lua_rawget( L, -2 );
        lua_pushvalue( L, 1 );
        lua_pushvalue( L, 2 );

        if ( lua_isfunction( L, -1 ) ) {
            lua_call( L, 2, 1 );
            return 1;
        }
        else if ( lua_istable( L, -1 ) ) {
            lua_gettable( L, -3 );
            return 1;
        }
    }

    lua_pushnil( L );
    return 1;
}

/* Module newindex function
*/
static int module_newindex_event( lua_State* L ) {
    lua_pushstring( L, ".set" );
    lua_rawget( L, -4 );

    if ( lua_istable( L, -1 ) ) {
        lua_pushvalue( L, 2 ); /* key */
        lua_rawget( L, -2 );

        if ( lua_iscfunction( L, -1 ) ) {
            lua_pushvalue( L, 1 ); /* only to be compatible with non-static vars */
            lua_pushvalue( L, 3 ); /* value */
            lua_call( L, 2, 0 );
            return 0;
        }
    }

    /* call old newindex meta event */
    if ( lua_getmetatable( L, 1 ) && lua_getmetatable( L, -1 ) ) {
        lua_pushstring( L, "__newindex" );
        lua_rawget( L, -2 );

        if ( lua_isfunction( L, -1 ) ) {
            lua_pushvalue( L, 1 );
            lua_pushvalue( L, 2 );
            lua_pushvalue( L, 3 );
            lua_call( L, 3, 0 );
        }
    }

    lua_settop( L, 3 );
    lua_rawset( L, -3 );
    return 0;
}

/* Class index function
    * If the object is a userdata (ie, an object), it searches the field in
    * the alternative table stored in the corresponding "ubox" table.
*/
static int class_index_event( lua_State* L ) {
    int t = lua_type( L, 1 );

    if ( t == LUA_TUSERDATA ) {
        /* Access alternative table */
#ifdef LUA_VERSION_NUM /* new macro on version 5.1 */
#if LUA_VERSION_NUM > 501
        lua_getuservalue( L, 1 );
#else
        lua_getfenv( L, 1 );
#endif

        if ( !lua_rawequal( L, -1, TOLUA_NOPEER ) ) {
            lua_pushvalue( L, 2 ); /* key */
            lua_gettable( L,
                          -2 ); /* on lua 5.1, we trade the "tolua_peers" lookup for a gettable call */

            if ( !lua_isnil( L, -1 ) ) {
                return 1;
            }
        };

#else
        lua_pushstring( L, "tolua_peers" );

        lua_rawget( L, LUA_REGISTRYINDEX );     /* stack: obj key ubox */

        lua_pushvalue( L, 1 );

        lua_rawget( L, -2 );                    /* stack: obj key ubox ubox[u] */

        if ( lua_istable( L, -1 ) ) {
            lua_pushvalue( L, 2 ); /* key */
            lua_rawget( L, -2 );                   /* stack: obj key ubox ubox[u] value */

            if ( !lua_isnil( L, -1 ) ) {
                return 1;
            }
        }

#endif
        lua_settop( L, 2 );                     /* stack: obj key */
        /* Try metatables */
        lua_pushvalue( L, 1 );                  /* stack: obj key obj */

        while ( lua_getmetatable( L, -1 ) ) {
            /* stack: obj key obj mt */
            lua_remove( L, -2 );                   /* stack: obj key mt */

            if ( lua_isnumber( L, 2 ) ) {          /* check if key is a numeric value */
                /* try operator[] */
                lua_pushstring( L, ".geti" );
                lua_rawget( L, -2 );                   /* stack: obj key mt func */

                if ( lua_isfunction( L, -1 ) ) {
                    lua_pushvalue( L, 1 );
                    lua_pushvalue( L, 2 );
                    lua_call( L, 2, 1 );
                    return 1;
                }
            }
            else {
                lua_pushvalue( L, 2 );                 /* stack: obj key mt key */
                lua_rawget( L, -2 );                   /* stack: obj key mt value */

                if ( !lua_isnil( L, -1 ) ) {
                    return 1;
                }
                else {
                    lua_pop( L, 1 );
                }

                /* try C/C++ variable */
                lua_pushstring( L, ".get" );
                lua_rawget( L, -2 );                   /* stack: obj key mt tget */

                if ( lua_istable( L, -1 ) ) {
                    lua_pushvalue( L, 2 );
                    lua_rawget( L, -2 );                   /* stack: obj key mt value */

                    if ( lua_iscfunction( L, -1 ) ) {
                        lua_pushvalue( L, 1 );
                        lua_pushvalue( L, 2 );
                        lua_call( L, 2, 1 );
                        return 1;
                    }
                    else if ( lua_istable( L, -1 ) ) {
                        /* deal with array: create table to be returned and cache it in ubox */
                        void* u = *( ( void** )lua_touserdata( L, 1 ) );
                        lua_newtable( L );              /* stack: obj key mt value table */
                        lua_pushstring( L, ".self" );
                        lua_pushlightuserdata( L, u );
                        lua_rawset( L, -3 );            /* store usertype in ".self" */
                        lua_insert( L, -2 );            /* stack: obj key mt table value */
                        lua_setmetatable( L, -2 );      /* set stored value as metatable */
                        lua_pushvalue( L, -1 );         /* stack: obj key met table table */
                        lua_pushvalue( L, 2 );          /* stack: obj key mt table table key */
                        lua_insert( L, -2 );            /*  stack: obj key mt table key table */
                        storeatubox( L, 1 );            /* stack: obj key mt table */
                        return 1;
                    }
                }
            }

            lua_settop( L, 3 );
        }

        lua_pushnil( L );
        return 1;
    }
    else if ( t == LUA_TTABLE ) {
        module_index_event( L );
        return 1;
    }

    lua_pushnil( L );
    return 1;
}

/* Newindex function
    * It first searches for a C/C++ varaible to be set.
    * Then, it either stores it in the alternative ubox table (in the case it is
    * an object) or in the own table (that represents the class or module).
*/
static int class_newindex_event( lua_State* L ) {
    int t = lua_type( L, 1 );

    if ( t == LUA_TUSERDATA ) {
        /* Try accessing a C/C++ variable to be set */
        lua_getmetatable( L, 1 );

        while ( lua_istable( L, -1 ) ) {         /* stack: t k v mt */
            if ( lua_isnumber( L, 2 ) ) {          /* check if key is a numeric value */
                /* try operator[] */
                lua_pushstring( L, ".seti" );
                lua_rawget( L, -2 );                   /* stack: obj key mt func */

                if ( lua_isfunction( L, -1 ) ) {
                    lua_pushvalue( L, 1 );
                    lua_pushvalue( L, 2 );
                    lua_pushvalue( L, 3 );
                    lua_call( L, 3, 0 );
                    return 0;
                }
            }
            else {
                lua_pushstring( L, ".set" );
                lua_rawget( L, -2 );                   /* stack: t k v mt tset */

                if ( lua_istable( L, -1 ) ) {
                    lua_pushvalue( L, 2 );
                    lua_rawget( L, -2 );                  /* stack: t k v mt tset func */

                    if ( lua_iscfunction( L, -1 ) ) {
                        lua_pushvalue( L, 1 );
                        lua_pushvalue( L, 3 );
                        lua_call( L, 2, 0 );
                        return 0;
                    }

                    lua_pop( L, 1 );                       /* stack: t k v mt tset */
                }

                lua_pop( L, 1 );                        /* stack: t k v mt */

                if ( !lua_getmetatable( L, -1 ) ) {     /* stack: t k v mt mt */
                    lua_pushnil( L );
                }

                lua_remove( L, -2 );                    /* stack: t k v mt */
            }
        }

        lua_settop( L, 3 );                       /* stack: t k v */
        /* then, store as a new field */
        storeatubox( L, 1 );
    }
    else if ( t == LUA_TTABLE ) {
        module_newindex_event( L );
    }

    return 0;
}

static int class_call_event( lua_State* L ) {
    if ( lua_istable( L, 1 ) ) {
        lua_pushstring( L, ".call" );
        lua_rawget( L, 1 );

        if ( lua_isfunction( L, -1 ) ) {
            lua_insert( L, 1 );
            lua_call( L, lua_gettop( L ) - 1, 1 );
            return 1;
        };
    };

    tolua_error( L, "Attempt to call a non-callable object.", NULL );

    return 0;
};

static int do_operator( lua_State* L, const char* op ) {
    if ( lua_isuserdata( L, 1 ) ) {
        /* Try metatables */
        lua_pushvalue( L, 1 );                  /* stack: op1 op2 */

        while ( lua_getmetatable( L, -1 ) ) {
            /* stack: op1 op2 op1 mt */
            lua_remove( L, -2 );                   /* stack: op1 op2 mt */
            lua_pushstring( L, op );               /* stack: op1 op2 mt key */
            lua_rawget( L, -2 );                   /* stack: obj key mt func */

            if ( lua_isfunction( L, -1 ) ) {
                lua_pushvalue( L, 1 );
                lua_pushvalue( L, 2 );
                lua_call( L, 2, 1 );
                return 1;
            }

            lua_settop( L, 3 );
        }
    }

    tolua_error( L, "Attempt to perform operation on an invalid operand", NULL );
    return 0;
}

static int class_add_event( lua_State* L ) {
    return do_operator( L, ".add" );
}

static int class_sub_event( lua_State* L ) {
    return do_operator( L, ".sub" );
}

static int class_mul_event( lua_State* L ) {
    return do_operator( L, ".mul" );
}

static int class_div_event( lua_State* L ) {
    return do_operator( L, ".div" );
}

static int class_lt_event( lua_State* L ) {
    return do_operator( L, ".lt" );
}

static int class_le_event( lua_State* L ) {
    return do_operator( L, ".le" );
}

static int class_eq_event( lua_State* L ) {
    /* copying code from do_operator here to return false when no operator is found */
    if ( lua_isuserdata( L, 1 ) ) {
        /* Try metatables */
        lua_pushvalue( L, 1 );                  /* stack: op1 op2 */

        while ( lua_getmetatable( L, -1 ) ) {
            /* stack: op1 op2 op1 mt */
            lua_remove( L, -2 );                   /* stack: op1 op2 mt */
            lua_pushstring( L, ".eq" );               /* stack: op1 op2 mt key */
            lua_rawget( L, -2 );                   /* stack: obj key mt func */

            if ( lua_isfunction( L, -1 ) ) {
                lua_pushvalue( L, 1 );
                lua_pushvalue( L, 2 );
                lua_call( L, 2, 1 );
                return 1;
            }

            lua_settop( L, 3 );
        }
    }

    lua_settop( L, 3 );
    lua_pushboolean( L, 0 );
    return 1;
}

/*
static int class_gc_event (lua_State* L)
{
    void* u = *((void**)lua_touserdata(L,1));
    fprintf(stderr, "collecting: looking at %p\n", u);
    lua_pushstring(L,"tolua_gc");
    lua_rawget(L,LUA_REGISTRYINDEX);
    lua_pushlightuserdata(L,u);
    lua_rawget(L,-2);
    if (lua_isfunction(L,-1))
    {
        lua_pushvalue(L,1);
        lua_call(L,1,0);
        lua_pushlightuserdata(L,u);
        lua_pushnil(L);
        lua_rawset(L,-3);
    }
    lua_pop(L,2);
    return 0;
}
*/
TOLUA_API int class_gc_event( lua_State* L ) {
    if ( lua_type( L, 1 ) == LUA_TUSERDATA ) {
        void* u = *( ( void** )lua_touserdata( L, 1 ) );
        int top;
        /*fprintf(stderr, "collecting: looking at %p\n", u);*/
        /*
        lua_pushstring(L,"tolua_gc");
        lua_rawget(L,LUA_REGISTRYINDEX);
        */
        lua_pushstring( L, "tolua_gc" );
        lua_rawget( L, LUA_REGISTRYINDEX ); /* gc */
        lua_pushlightuserdata( L, u );
        lua_rawget( L, -2 );         /* stack: gc umt    */
        lua_getmetatable( L, 1 );    /* stack: gc umt mt */
        /*fprintf(stderr, "checking type\n");*/
        top = lua_gettop( L );

        if ( tolua_fast_isa( L, top, top - 1,
                             lua_upvalueindex( 2 ) ) ) { /* make sure we collect correct type */
            /*fprintf(stderr, "Found type!\n");*/
            /* get gc function */
            lua_pushliteral( L, ".collector" );
            lua_rawget( L, -2 );        /* stack: gc umt mt collector */

            if ( lua_isfunction( L, -1 ) ) {
                /*fprintf(stderr, "Found .collector!\n");*/
            }
            else {
                lua_pop( L, 1 );
                /*fprintf(stderr, "Using default cleanup\n");*/
                lua_pushcfunction( L, tolua_default_collect );
            }

            lua_pushvalue( L, 1 );      /* stack: gc umt mt collector u */
            lua_call( L, 1, 0 );
            lua_pushlightuserdata( L, u ); /* stack: gc umt mt u */
            lua_pushnil( L );           /* stack: gc umt mt u nil */
            lua_rawset( L, -5 );        /* stack: gc umt mt */
        }

        lua_pop( L, 3 );
    }

    return 0;
}


/* Register module events
    * It expects the metatable on the top of the stack
*/
TOLUA_API void tolua_moduleevents( lua_State* L ) {
    lua_pushstring( L, "__index" );
    lua_pushcfunction( L, module_index_event );
    lua_rawset( L, -3 );
    lua_pushstring( L, "__newindex" );
    lua_pushcfunction( L, module_newindex_event );
    lua_rawset( L, -3 );
}

/* Check if the object on the top has a module metatable
*/
TOLUA_API int tolua_ismodulemetatable( lua_State* L ) {
    int r = 0;

    if ( lua_getmetatable( L, -1 ) ) {
        lua_pushstring( L, "__index" );
        lua_rawget( L, -2 );
        r = ( lua_tocfunction( L, -1 ) == module_index_event );
        lua_pop( L, 2 );
    }

    return r;
}

/* Register class events
    * It expects the metatable on the top of the stack
*/
TOLUA_API void tolua_classevents( lua_State* L ) {
    lua_pushstring( L, "__index" );
    lua_pushcfunction( L, class_index_event );
    lua_rawset( L, -3 );
    lua_pushstring( L, "__newindex" );
    lua_pushcfunction( L, class_newindex_event );
    lua_rawset( L, -3 );
    lua_pushstring( L, "__add" );
    lua_pushcfunction( L, class_add_event );
    lua_rawset( L, -3 );
    lua_pushstring( L, "__sub" );
    lua_pushcfunction( L, class_sub_event );
    lua_rawset( L, -3 );
    lua_pushstring( L, "__mul" );
    lua_pushcfunction( L, class_mul_event );
    lua_rawset( L, -3 );
    lua_pushstring( L, "__div" );
    lua_pushcfunction( L, class_div_event );
    lua_rawset( L, -3 );
    lua_pushstring( L, "__lt" );
    lua_pushcfunction( L, class_lt_event );
    lua_rawset( L, -3 );
    lua_pushstring( L, "__le" );
    lua_pushcfunction( L, class_le_event );
    lua_rawset( L, -3 );
    lua_pushstring( L, "__eq" );
    lua_pushcfunction( L, class_eq_event );
    lua_rawset( L, -3 );
    lua_pushstring( L, "__call" );
    lua_pushcfunction( L, class_call_event );
    lua_rawset( L, -3 );
    lua_pushstring( L, "__gc" );
    lua_pushstring( L, "tolua_gc_event" );
    lua_rawget( L, LUA_REGISTRYINDEX );
    /*lua_pushcfunction(L,class_gc_event);*/
    lua_rawset( L, -3 );
}

